package glmodel;

import java.util.ArrayList;
import ui.Message;
import java.nio.*;

/**
 * Holds a mesh object containing triangles and vertices. Verts and Triangles
 * are stored as ArrayLists for flexibility and also as arrays for speedier
 * processing. The rebuild() function converts the ArrayLists to arrays, assigns
 * neighbor triangles to each vert, and recalculates normals. Handles normal
 * smoothing, and preserves sharp edges. see projectVerts() for setting screen
 * positions (Zdepth) of verts see sortTriangles() for Z depth sorting see
 * regenerateNormals() to calculate smoothed normals oct 2008: added funcs to
 * handle groups sep 2008: changed gluProject() params due to lwjgl 2.0 changes
 * jun 2006: add makeclone()
 */
class GL_Mesh
{
	// TEMPORARY lists hold verts and tris while mesh is being loaded or built dynamically
	public ArrayList		vertexData			= new ArrayList();
	public ArrayList		triangleData		= new ArrayList();

	// Arrays hold verts and tris after mesh is done being loaded or built (see rebuild())
	public GL_Vertex[]		vertices;
	protected GL_Triangle[]	triangles;

	private int				numVertices			= 0;
	private int				numTriangles		= 0;

	protected String			name				= "";													// This object's name

	// calculate the cosine of the smoothing angle (in degrees) (see registerSmoothNeighbors())
	private float			maxSmoothingAngle	= 89f;
	private float			cos_angle			= (float) Math.cos(Math.toRadians(maxSmoothingAngle));

	// name of material file from .obj file (or null)
	protected String			materialLibeName	= null;
	protected GLMaterial[]		materials			= null;

	// For managing triangle groups.  See makeGroups().
	// start with 1 group (default)
	private GL_Triangle[][]			groupFaces			= new GL_Triangle[1][];
	private String[]				groupNames			= new String[]{"default"};
	private String[]				groupMaterialNames	= new String[]{null};
	private int						currentGroup		= 0;

	// temporary buffer used by projectVerts()
	private FloatBuffer		projectedVert		= allocFloats(16);

	public GL_Mesh()
	{
	}

	/**
	 * the smoothing angle determines how smooth or faceted a model will appear.
	 * If the angle between the normals of two neighboring faces is greater than
	 * the smoothing angle then the faces will be smoothed (the vertex normals
	 * at the edge of the faces will be averaged.
	 * 
	 * <PRE>
	 *               N1        example: the angle between face normals 1 and 2
	 *              |                   is 90 degrees.  If maxSmoothingAngle is 80
	 *         _____|___                these faces will have a hard edge.  If
	 *      face1       |___ N2         maxSmoothingAngle is 100 they will be smoothed.
	 *                  |
	 *                  |
	 *              face2
	 * </PRE>
	 * 
	 * @param maxSmoothingAngle
	 */
	public void setSmoothingAngle(float maxSmoothingAngle)
	{
		this.maxSmoothingAngle = maxSmoothingAngle;
		cos_angle = (float) Math.cos(Math.toRadians(maxSmoothingAngle));
	}

	/**
	 * get the vertex for the given index (aka vertex ID) this assumes that
	 * rebuild() was run to build the vertices array from temporary vertexData
	 * 
	 * @param id
	 *            of vertex
	 * @return the vertex
	 */
	protected GL_Vertex vertex(int id)
	{
		if (vertexData != null)
		{
			return (GL_Vertex) vertexData.get(id);
		}
		else
		{
			return vertices[id];
		}
	}

	/**
	 * add vertex to arraylist. rebuild() will copy verts into an array for
	 * faster retrieval. tag each vert with an ID (its index into the vertex
	 * array) this is needed for makeClone() to correctly clone this mesh
	 * 
	 * @param newVertex
	 */
	private void addVertex(GL_Vertex newVertex)
	{
		newVertex.ID = vertexData.size();
		vertexData.add(newVertex);
	}

	protected void addVertex(float x, float y, float z)
	{
		addVertex(new GL_Vertex(x, y, z));
	}

	protected void addTriangle(GL_Triangle newTriangle)
	{
		triangleData.add(newTriangle);
	}

	

	/**
	 * Copy vertex and triangle data into arrays for faster access. Find the
	 * neighbor triangles for each vertex. This data should not change once the
	 * mesh is loaded, so we call rebuild() only when the object is imported.
	 */
	protected void rebuild()
	{
		if (vertexData == null || triangleData == null)
		{
			Message.getInstance().msg("GL_Mesh.rebuild(): can't rebuild mesh after finalize() was run.");
			return;
		}

		// Copy vertices to array
		numVertices = vertexData.size();
		vertices = new GL_Vertex[numVertices];
		for (int i = 0; i < numVertices; i++)
		{
			vertices[i] = vertex(i);
			vertices[i].ID = i; // vert ID is the index into vert array
			vertices[i].resetNeighbors(); // clear the neighbor triangle list
		}

		// Copy triangles to array
		GL_Triangle tri;
		numTriangles = triangleData.size();
		triangles = new GL_Triangle[numTriangles];
		for (int i = 0; i < numTriangles; i++)
		{
			triangles[i] = tri = (GL_Triangle) triangleData.get(i);
			tri.ID = i;
			// register the triangle as a "neighbor" of its verts
			tri.p1.addNeighborTri(tri);
			tri.p2.addNeighborTri(tri);
			tri.p3.addNeighborTri(tri);
		}
	}

	/**
	 * remove tempoarary vert and triangle arraylists. These lists are used to
	 * gather verts and triangles dynamically (ie. when loading or building a
	 * mesh algorithmically). Once the mesh is complete then call finalize() to
	 * convert the arraylists to fixed-length arrays, and to free up redundant
	 * arraylist memory. CAN'T REBUILD AGAIN once finalize has been run!
	 */
	public void finalize()
	{
		if (vertexData == null || triangleData == null)
		{
			Message.getInstance().msg("GL_Mesh.finalize(): looks like finalize() was already run.");
			return;
		}

		// convert vert and tri lists to arrays
		//rebuild();  // call this separately

		// remove vert and tri lists
		vertexData.clear();
		triangleData.clear();
		vertexData = triangleData = null;

		// optimize the vertex neighbor lists
		for (int i = 0; i < vertices.length; i++)
		{
			vertices[i].neighborTris.trimToSize();
		}
	}

	///////////////////////////////////////////////
	// Since a vertex can be shared by several triangles
	// and each triangle may have a different normal (ie. two triangles
	// form a 90 degree edge), then we need to store the neighboring
	// triangles for each vertex in each triangle.  When a vertex is
	// shared by several triangles, each triangle can have a different
	// normal for that vert, based on the neighbors of that triangle.

	/**
	 * For the given Vert in the given Triangle, make a list of triangles that
	 * are neighbors to the given Triangle. Only count as neighbors those
	 * triangles that form a smooth surface with this triangle, meaning the
	 * angle between this triangle and the neighbor triangle is > 90 degrees
	 * (the actual min degrees value is in cos_angle). Requires that rebuild()
	 * has been run so that the vertex has a list of neighbor triangles
	 * populated (see addNeighborTri()), and the triangle face normals have been
	 * calculated. (see GL_Triangle.regenerateNormal()).
	 */
	public void registerSmoothNeighbors(ArrayList neighborTris, GL_Vertex v, GL_Triangle t)
	{
		GL_Triangle neighborTri;
		// any triangle that is neighbor to the vert, is neighbor to that verts parent triangle
		// for each triangle that touches the vertex...
		for (int i = 0; i < v.neighborTris.size(); i++)
		{
			neighborTri = (GL_Triangle) v.neighborTris.get(i);
			// Test for > 90 degree angle between triangle t and this triangle
			if (GL_Triangle.onSameSurface(t, neighborTri, cos_angle))
			{
				// if the angle is obtuse enough, we'll smooth the two triangles together
				// add the neighbor triangle to a list of "smooth neighbors"
				if (!neighborTris.contains(neighborTri))
				{
					neighborTris.add(neighborTri);
				}
			}
		}
		if (neighborTris.size() == 0)
		{
			// no neighbors found.  Use the verts parent triangle
			neighborTris.add(t);
		}
	}

	/**
	 * Recalculate normals for each vertex in each triangle. This allows a
	 * vertex to have a different normal for each triangle it's in (so we can
	 * have sharp edges or smooth surfaces). Requires that neighoring triangles
	 * have already been set.
	 * 
	 * @see rebuild(), registerNeighbors(), setSmoothingAngle()
	 */
	protected void regenerateNormals()
	{
		GL_Triangle tri;
		// first calculate all triangle (face) normals
		for (int i = 0; i < numTriangles; i++)
		{
			triangles[i].recalcFaceNormal();
		}
		// Register the "smooth" neighbor triangles for each vert in
		// each triangle. Triangles that share verts are neighbors.
		// A "smooth" neighbor is a triangle that will be treated
		// as part of the same surface as this triangle.
		// See setSmoothingAngle() to set the angle at which triangles
		// are treated as two separate surfaces.
		for (int i = 0; i < numTriangles; i++)
		{
			tri = triangles[i];
			tri.resetNeighbors();
			registerSmoothNeighbors(tri.neighborsP1, tri.p1, tri);
			registerSmoothNeighbors(tri.neighborsP2, tri.p2, tri);
			registerSmoothNeighbors(tri.neighborsP3, tri.p3, tri);
		}
		// next calculate normals for each vert in each triangle.
		// the vert normal is the average of it's neighbor triangle normals.
		for (int i = 0; i < numTriangles; i++)
		{
			tri = triangles[i];
			tri.norm1 = tri.recalcVertexNormal(tri.neighborsP1);
			tri.norm2 = tri.recalcVertexNormal(tri.neighborsP2);
			tri.norm3 = tri.recalcVertexNormal(tri.neighborsP3);
		}
	}

	/**
	 * Return minimum point in the mesh.
	 */
	private GL_Vector min()
	{
		if (numVertices == 0)
			return new GL_Vector(0f, 0f, 0f);
		float minX = vertices[0].pos.x;
		float minY = vertices[0].pos.y;
		float minZ = vertices[0].pos.z;
		for (int i = 0; i < numVertices; i++)
		{
			if (vertices[i].pos.x < minX)
				minX = vertices[i].pos.x;
			if (vertices[i].pos.y < minY)
				minY = vertices[i].pos.y;
			if (vertices[i].pos.z < minZ)
				minZ = vertices[i].pos.z;
		}
		return new GL_Vector(minX, minY, minZ);
	}

	/**
	 * Return maximum point in the mesh.
	 */
	private GL_Vector max()
	{
		if (numVertices == 0)
			return new GL_Vector(0f, 0f, 0f);
		float maxX = vertices[0].pos.x;
		float maxY = vertices[0].pos.y;
		float maxZ = vertices[0].pos.z;
		for (int i = 0; i < numVertices; i++)
		{
			if (vertices[i].pos.x > maxX)
				maxX = vertices[i].pos.x;
			if (vertices[i].pos.y > maxY)
				maxY = vertices[i].pos.y;
			if (vertices[i].pos.z > maxZ)
				maxZ = vertices[i].pos.z;
		}
		return new GL_Vector(maxX, maxY, maxZ);
	}

	/**
	 * Return the center point of the mesh.
	 */
	public GL_Vector getCenter()
	{
		GL_Vector max = max();
		GL_Vector min = min();
		return new GL_Vector((max.x + min.x) / 2f, (max.y + min.y) / 2f, (max.z + min.z) / 2f);
	}

	/**
	 * Returns the dimensions of this mesh.
	 */
	public GL_Vector getDimension()
	{
		GL_Vector max = max();
		GL_Vector min = min();
		return new GL_Vector(max.x - min.x, max.y - min.y, max.z - min.z);
	}

	/**
	 * return the vertex array
	 */
	public GL_Vertex[] getVertexArray()
	{
		return vertices;
	}


	

	/**
	 * Return true if the point px,py is inside the triangle created by the
	 * three triangle coordinates.
	 */
	private static boolean pointInTriangle(float px, float py, float x1, float y1, float x2, float y2, float x3, float y3)
	{
		float b0 = ((x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1));
		float b1 = ((x2 - px) * (y3 - py) - (x3 - px) * (y2 - py)) / b0;
		float b2 = ((x3 - px) * (y1 - py) - (x1 - px) * (y3 - py)) / b0;
		float b3 = ((x1 - px) * (y2 - py) - (x2 - px) * (y1 - py)) / b0;
		if (b1 > 0 && b2 > 0 && b3 > 0)
		{
			return true;
		}
		return false;
	}

	public String toString()
	{
		StringBuffer buffer = new StringBuffer();
		buffer.append("<object id=" + name + ">\r\n");
		for (int i = 0; i < numVertices; i++)
		{
			buffer.append(vertices[i].toString());
		}
		return buffer.toString();
	}

	private static final int	SIZE_FLOAT	= 4;

	private static FloatBuffer allocFloats(int howmany)
	{
		FloatBuffer fb = ByteBuffer.allocateDirect(howmany * SIZE_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();
		return fb;
	}

	//========================================================================
	// Functions to organize faces into groups.
	//========================================================================

	/**
	 * Allocate arrays to hold <num> groups. The GL_OBJ_Importer calls this
	 * once, after it has loaded the OBJ and knows how many groups it has. Use
	 * initGroups() to allocate triangle arrays for each group.
	 * 
	 * @param num
	 *            number of groups to allocate
	 * @see initGroup()
	 */
	protected void makeGroups(int num)
	{
		groupFaces = new GL_Triangle[num][];
		groupNames = new String[num];
		groupMaterialNames = new String[num];
	}

	/**
	 * allocate triangle array for a group. Also set name and material name.
	 * 
	 * @param groupNum
	 *            number of group to init
	 * @param name
	 *            group name
	 * @param materialName
	 *            group's material
	 * @param numTriangles
	 *            number of triangles in the group
	 * @see makeGroups()
	 */
	protected void initGroup(int groupNum, String name, String materialName, int numTriangles)
	{
		groupFaces[groupNum] = new GL_Triangle[numTriangles];
		groupNames[groupNum] = name;
		groupMaterialNames[groupNum] = materialName;
		currentGroup = groupNum;
	}

	/**
	 * add triangle to given group. used by Importer to load mesh groups. Groups
	 * had to be allocated first by makeGroups() and inited with initGroup().
	 * 
	 * @param newTriangle
	 * @param groupNum
	 * @param triangleNum
	 * @see makeGroups()
	 * @see setGroup()
	 */
	protected void addTriangle(GL_Triangle newTriangle, int groupNum, int triangleNum)
	{
		// set IDs into the triangle
		newTriangle.ID = triangleData.size();
		newTriangle.groupID = groupNum;
		// store the triangle
		triangleData.add(newTriangle);
		groupFaces[groupNum][triangleNum] = newTriangle;
	}

}